package paper.http;

import java.io.*;

import paper.batch.MD5;

/**
 * This class will read bytes from a CRLF separated inputstream, chop it
 * into lines, while transparently copying the data to an outputstream.
 * This class is designed to preprocess a stream for multipart decoding.
 *
 * The stream has a cursor that points to a current line. There are methods
 * to obtain this current line, to compare the line against an array of
 * bytes, and to move the cursor on to the next line. A line always
 * includes the CRLF before and CRLF after the current line, hence lines
 * overlap. A line is copied onto the outputstream excluding the trailing
 * CRLF when moving the cursor on. When setting the outputstream, it will
 * not copy the current line, and not copy the CRLF trailing the current
 * output line.
 *
 * When it encoutners lines longer than a preset limit, they
 * are passed onto the other stream, but silently skipped by the cursor -
 * the limit should be high enough to store interesting lines, such as
 * ---asdas--- separators.
 */
public class StreamReader {

    /*
     * Properties of the stream, the input to read from , the output to
     * write to (always poitns to something, but may change halfway), and
     * the maximum line length that we accept. Does not need to be store,
     * but affects the buffer length allocated on initialisation.
     */
    InputStream input;
    OutputStream output;
    int maxLineLength;
    MD5 md5;

    /*
     * The buffer. An array of bytes of a fixed length, and a bufferLength
     * indicating how long this array is. The length variable tells us how
     * many bytes there are at present in the buffer. Typically
     * bufferLength shall be set to something large, and at least
     * maxLineLength + 4.
     */
    int bufferLength;
    byte[] buffer;
    int length;

    /*
     * The cursor: start is an index in the buffer that points to the CR of
     * the start of the line, end is an index that points to the LF of the
     * end of the line.
     */

    int start = -1, end = -1;

    /**
     * Instantiate a new StreamReader. Upon calling this method the cursor
     * is set on the first line; no data will be copied tothe outputStream.
     * The output Stream is set to the null outputstream.
     * @param input The InputStream to read data from.
     * @param maxLineLength The maximum line length that the cursor will
     * point to. Longer lines are skipped by the cursor, but copied to the
     * OutputStream.
     * @throws IOException if the inputStream is throwing an IOException.
     */
    public StreamReader(InputStream input, int maxLineLength) throws IOException {
        this.input = input;
        this.maxLineLength = maxLineLength;
        this.bufferLength = maxLineLength+4; // fixme: set to 8192 if less.
        this.output = null;
        this.buffer = new byte[bufferLength];
        this.md5 = null;
        fillBuffer();
        moveToNextLine();
    }

    /**
     * Fills the buffer with data. It fills it up to as many bytes as it
     * can fit in there; and returns when either the buffer is full, or the
     * InputStream is exhausted; whichever comes first. It adjust the
     * length accordingly.
     * @throws IOException if the inputStream is throwing an IOException.
     */
    private void fillBuffer() throws IOException {
        while (true) {
            int space = bufferLength - length;
            if (space == 0) {
//                System.out.println("Buffer full");
                return;
            }
            int bytesRead = input.read(buffer, length, space);
            if (bytesRead <= 0) {
//                System.out.println("Buffer read returns " + bytesRead);
                return;
            }
            length += bytesRead;
//            System.out.println("Read " + bytesRead + " now " + length);
        }
    }

    /**
     * Flushes the buffer from start to some place. This will write that
     * part of the buffer to the underlying outputStream.
     *
     * @param where Specifies the index of the array upto which the data
     * should be flushed. That is, buffer[where] will not be flushed.
     * @throws IOException If the underlying inputstream is unhappy, or if
     * no CRLF is found until the end, this method throws an IOException.
     */
    private void flush(int where) throws IOException {
        int localStart = start;
        if (start < 0) {
                localStart = 0;
        }
        if (output != null) {
            output.write(buffer, localStart, where-localStart);
        }
        if (md5 != null) {
            md5.update(buffer, localStart, where-localStart);
        }
    }

    /**
     * Finds the start of a line in the buffer, starting from 0. It will
     * flush the existing buffer from start onwards, and will refill the
     * buffer and flush as often as is required. No assumptions are made
     * about end.
     *
     * On return, all data up to start will be flushed. Start will point to a CR, and end will point to the NL after that.
     * @throws IOException, if the underlying inputstream is unhappy, or if
     * no CRLF is found until the end.
     */
    private void findStart() throws IOException {
        while (true) {
            flush(length-1);
            buffer[0] = buffer[length-1];
            length = 1;
            start  = 0;
            fillBuffer();
            for(end = 1;  end < length; end++) {
                if (buffer[end-1] == '\r' && buffer[end] == '\n') {
                    flush(end-1);
                    start = end-1;
                    return;
                }
            }
        }
    }

    /**
     * Moves the cursor onto the next line. This may cause a few megabytes
     * of data to be flushed through to the output stream.
     * @throws IOException if the inputStream throws an IOException, or if
     * the inputStream does not end with a CRLF
     */
    public void moveToNextLine() throws IOException {
        flush(end-1);
        start = end-1;    // This is by definition the start of the next line.
        while (true) {    // end does not point to LF
            end++;
            if (end >= length) {       // We have run out of buffer space.
                if (start == 0) {      // Line longer than buffer...
                    findStart();       // find a suitable starting point
                    continue;          // Adn move on to advance end.
                } else {
                    int shift = start; // Shift buffer by this many places.
                    System.arraycopy(buffer, shift, buffer, 0, length - shift);
                    length -= shift;   // Adjust buffer length
                    end    -= shift;   // Adjust cursor end
                    start  -= shift;   // Adjust cursor start (will be zero!)
                    fillBuffer();
                    if (end >= length) {
                        throw new IOException("Input stream ends badly");
                    }
                }
            }
            // starts point to start, end is at most length-1.
            if (end >= 1 && buffer[end-1] == '\r' && buffer[end] == '\n') {
//                System.out.println("Found line >" + new String(buffer,start+2,end-start-3) + "<");
                return;
            }
        }
        
    }

    /**
     * Obtains the current line, includign the CRLF at the beginning and
     * the end. Hence, the minimum length returned is 4 bytes. The line is
     * returned as an array of bytes, ready to be input in, for example,
     * lineEquals.
     * @returns The line as an array of bytes,
     */
    public byte[] getLine() {
        int lineLength = end-start+1;
        byte[] line = new byte[lineLength];
        if (start == -2) {
            line[0] = '\r';
            line[1] = '\n';
            System.arraycopy(buffer, 0, line, 2, lineLength-2); // ugh.
        } else {
            System.arraycopy(buffer, start, line, 0, lineLength);
        }
        return line;
    }


    /**
     * Compares the current line against an array of bytes. The comparison
     * is made including the CRLF on either side of the line.
     * @param compare The array of bytes to compare the current line against.
     * @returns true if the line is the same as compare, false otherwise.
     */
    public boolean lineEquals(byte[] compare) {
        if (compare.length != end-start+1) {
            return false;
        }
        for (int i = 0; i < compare.length; i++) {
            if (compare[i] != buffer[start+i]) {
                return false;
            }
        }
        return true;
    }

    /**
     * Skips a number of characters in the inputstream. To be called before
     * you switch output stream and start collating characters in a
     * multipart. Typically called with 4, in order to SKIP CRLFCRLF.
     * @param numChar the number of characters to be skipped. This should
     * be less than maxLineLength,
     * @throws IOException if the skip is more than allowed.
     */
    public void skip(int numChar) throws IOException {
        start += numChar;
        if (start > length) {      // fixme: this works for this purpose,
                                   // but cannot reliably skip more than 4.
            throw new IOException("Skipped too far.");
        }
        end += numChar-2;
        if (start > length+1) {    // end is pointing one too far.
            throw new IOException("Skipped too far. This shouldn't happen");
        }
    }

    /**
     * Changes the outputStream to the given outputStream. This function
     * closes the old outputStream. Calling this function implies that you
     * want to get rid of the CRLF at the beginning of the current line.
     * @param output The new outputStream.
     * @throws IOException if the current outputStream will not close.
     */
    public void setOutputStream(OutputStream output, MD5 md5)
        throws IOException {
        if (this.output != null) {
            this.output.close();
        }
        this.output = output;
        this.md5 = md5;
    }
}
